#include <poll.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <errno.h>

#include "gobang.h"
#include "chessboard_display_handler.h"
#include "connecting.h"
#include "utils.h"


#define kUSER_INPUT_BUFFER_SIZE 4
#define kSECONDS_PER_MINUTE 60
#define kMILLISECONDS_PER_SECOND 1000


const int INUPUT_TIMEOUT = 3;
const int NETWORK_TIMEOUT = 5;

#define PickSideAccordingMode(mode) ((mode) == kPLAYER_MODE_PASSIVE ? kCHESS_SIDE_WHITE : kCHESS_SIDE_BLACK);
#define GetOpponentSide(my_side) ((my_side) == kCHESS_SIDE_BLACK ? kCHESS_SIDE_WHITE : kCHESS_SIDE_BLACK)

typedef struct {
    uint8_t x : 4;
    uint8_t y : 4;
}ChessmanPosition;


typedef enum return_code {
    kRETURN_CODE_OK,
    kRETURN_CODE_TIMEOUT
}TRETURN_CODE;


#define map_letter_to_integer(letter) ((letter) - kGOBANG_COORDINATES_LETTER_START + 1)
#define map_integer_to_letter(integer) ((integer) - 1 + kGOBANG_COORDINATES_LETTER_START)

static ChessmanPosition gobangCoordinatesToChessmanPosition(GobangCoordinates gobang_coord)
{
    ChessmanPosition chessman_position;

    chessman_position.x = gobang_coord.number;
    chessman_position.y = map_letter_to_integer(gobang_coord.letter);

    return chessman_position;
}


static GobangCoordinates chessmanPositionToGobangCoordinates(ChessmanPosition chessman_position)
{
    GobangCoordinates gobang_coord;

    gobang_coord.number = chessman_position.x;
    gobang_coord.letter = map_integer_to_letter(chessman_position.y);

    return gobang_coord;
}


static GobangCoordinates generateRandomGobangCoordinates()
{
    GobangCoordinates gobang_coords;

    const int number_of_rows = kBOARD_BOUND_DOWN - kBOARD_BOUND_UP - 1;
    const int number_of_columns = kBOARD_BOUND_RIGHT - kBOARD_BOUND_LEFT - 1;

    gobang_coords.letter = rand() % number_of_columns + kGOBANG_COORDINATES_LETTER_START;
    gobang_coords.number = rand() % number_of_rows + kGOBANG_COORDINATES_NUMBER_START;

    return gobang_coords;
}


static GobangCoordinates randomPut(ChessBoard *chess_board, char side)
{
    int gobang_code;
    GobangCoordinates gobang_coords;

    do {
        gobang_coords = generateRandomGobangCoordinates();
        gobang_code = Gobang_TryPuttingChess(chess_board, side, gobang_coords);
    } while(gobang_code != kGOBANG_CODE_OK);

    return gobang_coords;
}


static GobangCoordinates inputTimeoutPunish(ChessBoard *chess_board, char side)
{
    GobangCoordinates gobang_coords = randomPut(chess_board, side);

    printf("Your input takes too long!\n");
    printf("Punishment: A chess was randomly put on %c%d!\n", gobang_coords.letter, gobang_coords.number);

    return gobang_coords;
}


static int pollUserInput(char *buffer, size_t max_size, int timeout)
{
    const struct pollfd mypoll = {
        .fd = STDIN_FILENO,
        .events = POLLIN
    };

    if (poll(&mypoll, 1, timeout * kSECONDS_PER_MINUTE * kMILLISECONDS_PER_SECOND) == 0) {
        return kRETURN_CODE_TIMEOUT;
    }
    
    fgets(buffer, max_size, stdin);
    stripNewlineCharacter(buffer);

    return kRETURN_CODE_OK;
}


static int waitOpponentChessmanPosition(int sockfd, struct sockaddr_in opponent_address, ChessmanPosition *new_chessman_position)
{
    socklen_t opponent_address_len = sizeof(opponent_address);

    if (recvfrom(sockfd, new_chessman_position, 1, MSG_WAITALL, &opponent_address, &opponent_address_len) < 0 &&
        errno == EWOULDBLOCK)
    {
        return kRETURN_CODE_TIMEOUT;
    }

    return kRETURN_CODE_OK;
}


static void sendMyGobangCoordinatesToOpponent(int sockfd, struct sockaddr_in opponent_address, GobangCoordinates gobang_coord)
{
    ChessmanPosition chessman_position = gobangCoordinatesToChessmanPosition(gobang_coord);
    sendto(sockfd, &chessman_position, sizeof(ChessmanPosition), MSG_CONFIRM, &opponent_address, sizeof(opponent_address));
}


static GobangCoordinates waitMyGobangCoordinates(ChessBoard *chess_board, char *input_buffer, char side)
{
    GobangCoordinates gobang_coords;
    int gobang_code = kGOBANG_CODE_OTHER_WRONG;
    char *chess_character = chess_character_table[ChessSideToTableIndex(side)];
    
    while (gobang_code != kGOBANG_CODE_OK) {                                                                                
        printf("%s >>> ", chess_character);                                                       
        fflush(stdout);   
        memset(input_buffer, 0, kUSER_INPUT_BUFFER_SIZE);                                                               

        if (pollUserInput(input_buffer, kUSER_INPUT_BUFFER_SIZE, INUPUT_TIMEOUT) == kRETURN_CODE_TIMEOUT) {
            return inputTimeoutPunish(chess_board, side);
        }

        gobang_code = Gobang_ParseGobangCoordinatesFromString(input_buffer, &gobang_coords);
        if (gobang_code == kGOBANG_CODE_OK) {               
            gobang_code = Gobang_TryPuttingChess(chess_board, side, gobang_coords);
        }

        Gobang_GobangCodeToString(gobang_code);
    }

    return gobang_coords;
}


static int opponentTurn(ChessBoard *chess_board, char opponent_side, int sockfd, struct sockaddr_in opponent_address)
{
    ChessmanPosition chessman_position;
    GobangCoordinates gobang_coord;

    if (waitOpponentChessmanPosition(sockfd, opponent_address, &chessman_position) == kRETURN_CODE_TIMEOUT) {
        printf("Network seems failed.\n");
        exit(EXIT_FAILURE);
    }

    gobang_coord = chessmanPositionToGobangCoordinates(chessman_position);
    printf("opponentonent put on %c%d\n", gobang_coord.letter, gobang_coord.number);
    Gobang_TryPuttingChess(chess_board, opponent_side, gobang_coord);
    ChessBoardDisplayHandler_DisplayChessBoard(chess_board);

    return Gobang_CheckGameWinOrNot(chess_board, gobang_coord);
}


static int myTurn(ChessBoard *chess_board, char side, int sockfd, struct sockaddr_in opponent_address)
{
    int gobang_code;
    GobangCoordinates gobang_coord;
    char input_buffer[kUSER_INPUT_BUFFER_SIZE] = {0};

    gobang_coord = waitMyGobangCoordinates(chess_board, input_buffer, side);
    ChessBoardDisplayHandler_DisplayChessBoard(chess_board);
    sendMyGobangCoordinatesToOpponent(sockfd, opponent_address, gobang_coord);

    gobang_code = Gobang_CheckGameWinOrNot(chess_board, gobang_coord);
    if (gobang_code == kGOBANG_CODE_ALREADY_WON) {
        printf("You won.\n");
        return kGOBANG_CODE_ALREADY_WON;
    }

    return kGOBANG_CODE_NO_ONE_WIN;
}


static void setNetworkFailTimeout(int sockfd, int timeout)
{
    struct timeval time_value;
    time_value.tv_sec = timeout * kSECONDS_PER_MINUTE;
    time_value.tv_usec = 0;

    setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &time_value, sizeof(time_value));
}


void Playing_StartGame(int sockfd, char mode, struct sockaddr_in opponent_address)
{
    int gobang_code = kGOBANG_CODE_NO_ONE_WIN;
    ChessBoard *chess_board = Gobang_NewChessBoard();
    
    char side = PickSideAccordingMode(mode);
    char opponent_side = GetOpponentSide(side);

    srand(time(NULL));

    setNetworkFailTimeout(sockfd, NETWORK_TIMEOUT);

    if (mode == kPLAYER_MODE_POSITIVE) {
        ChessBoardDisplayHandler_DisplayChessBoard(chess_board);
        myTurn(chess_board, side, sockfd, opponent_address);
    }
    
    while(gobang_code == kGOBANG_CODE_NO_ONE_WIN) {
        printf("Waiting opponentonent make chessman position.\n");
        gobang_code = opponentTurn(chess_board, opponent_side, sockfd, opponent_address);

        if (gobang_code == kGOBANG_CODE_ALREADY_WON) {
            printf("You lost.\n");
            break;
        }

        gobang_code = myTurn(chess_board, side, sockfd, opponent_address);
    }

    Gobang_DestoryChessBoard(chess_board);
}
